﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Hik.Communication.Scs.Communication;
using Hik.Communication.Scs.Communication.Messages;
using Hik.Communication.Scs.Communication.Protocols;

namespace CommonLib
{
    public enum enumHeader
    {
        FromServer = 0x4040,
        FromTracking = 0x2424
    }

    public enum enumCommand
    {
        Server_Confirms_Tracker_Login = 0x4000,
        Tracker_Login = 0x5000
        //Server_Confirms_tracker_login = 16384, //0x4000
        //Tracker_login = 20480 //0x5000
    }

    /// <summary>
    /// Default communication protocol between server and clients to send and receive a message.
    /// It uses .NET binary serialization to write and read messages.
    /// 
    /// A Message format:
    /// [Message Length (4 bytes)][Serialized Message Content]
    /// 
    /// If a message is serialized to byte array as N bytes, this protocol
    /// adds 4 bytes size information to head of the message bytes, so total length is (4 + N) bytes.
    /// 
    /// This class can be derived to change serializer (default: BinaryFormatter). To do this,
    /// SerializeMessage and DeserializeMessage methods must be overrided.
    /// </summary>
    public class TrackingWireProtocol : IScsWireProtocol
    {
        #region Private fields

        //Length = 177 bytes;
        //<$$ (2 bytes)><L ( 2 bytes )><ID (7 bytes)><command (2 bytes)><data (100 bytes)><checksum (2 bytes)><\r\n (2 bytes)>
        public byte[] _header = new byte[2]; //2 bytes
        public byte[] _length = new byte[2]; //2 bytes
        public byte[] _id = new byte[7]; //7 bytes
        public byte[] _command = new byte[2]; //2 bytes
        public byte[] _data = new byte[100]; //100 bytes
        public byte[] _checksum = new byte[2]; //2 bytes
        public byte[] _end = new byte[2]; //2 bytes

        /// <summary>
        /// Maximum length of a message.
        /// </summary>
        private const int MaxMessageLength = 128 * 1024 * 1024; //128 Megabytes.

        /// <summary>
        /// This MemoryStream object is used to collect receiving bytes to build messages.
        /// </summary>
        private MemoryStream _receiveMemoryStream;

        #endregion

        #region Constructor

        /// <summary>
        /// Creates a new instance of BinarySerializationProtocol.
        /// </summary>
        public TrackingWireProtocol()
        {
            _receiveMemoryStream = new MemoryStream();
        }

        #endregion

        #region My Custom Protocol

        /// <summary>
        /// Tạo packet protocol
        /// Protocol: <$$ (2 bytes)><L ( 2 bytes )><ID (7 bytes)><command (2 bytes)><data (160 bytes)><checksum (2 bytes)><\r\n (2 bytes)>
        /// </summary>
        /// <returns></returns>
        public byte[] CreateFullPacket()
        {
            //Lưu độ dài byte hiện tại
            int len = 0;

            //Tạo danh sách các bytes của packet
            _length = BitConverter.GetBytes((ushort)(_header.Length + _length.Length + _id.Length + _command.Length + _data.Length + _checksum.Length + _end.Length));

            //Khởi tạo packet có kích thước là _length
            byte[] packet = new byte[BitConverter.ToInt16(_length, 0)];

            //khởi tạo biến lưu packet dùng để tính checksum
            //checksum chỉ tính các byte của các _header, _length, _id, _command, _data (không tính các byte của _checksum)
            int checkSumLen = _header.Length + _length.Length + _id.Length + _command.Length + _data.Length;
            byte[] checkSumPacket = new byte[checkSumLen];

            //$$ : header
            _header.CopyTo(packet, 0);
            _header.CopyTo(checkSumPacket, 0);
            len += _header.Length;

            //<L> : lenght of all bytes for the packet
            _length.CopyTo(packet, len);
            _length.CopyTo(checkSumPacket, len);
            len += _length.Length;

            //<ID (7 bytes)>
            _id.CopyTo(packet, len);
            _id.CopyTo(checkSumPacket, len);
            len += _id.Length;

            //<command (2 bytes)>
            _command.CopyTo(packet, len);
            _command.CopyTo(checkSumPacket, len);
            len += _command.Length;

            //<data>
            if (_data.Length > 0)
            {
                _data.CopyTo(packet, len);
                _data.CopyTo(checkSumPacket, len);
                len += _data.Length;
            }

            //<checksum (2 bytes)>
            //calculate checksum by Crc16citt method
            CRC16CCITT objCrc = new CRC16CCITT();
            objCrc.Crc16Ccitt(InitialCrcValue.NonZero1);
            _checksum = objCrc.ComputeChecksumBytes(checkSumPacket);
            _checksum.CopyTo(packet, len);
            len += _checksum.Length;

            _end.CopyTo(packet, len);

            return packet;
        }

        /// <summary>
        /// Create a packet for client send to server
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private void InitSendingProtocol(byte[] data)
        {
            //PacketProtocol objPacket = new PacketProtocol();

            // header for packet from tracking unit to server

            _header = BitConverter.GetBytes((Int16)enumHeader.FromTracking);//new byte[] { 0x24, 0x24 };

            //id in hex code: 13612345678, unused bytes is 'f' or '0xff'
            _id = new byte[] { 0x13, 0x61, 0x23, 0x45, 0x67, 0x8f, 0xff };

            //command Tradker's login: 0x5000
            _command = BitConverter.GetBytes((ushort)enumCommand.Tracker_Login); ; // new byte[] { 0x00, 0x50 };

            //data
            if (data.Length > 0)
            {
                _data = new byte[data.Length];
                data.CopyTo(_data, 0);
            }

            //get end characters
            _end = new byte[] { 0x0d, 0x0a }; //Encoding.ASCII.GetBytes("\r\n");
        }

        #endregion

        #region IScsWireProtocol implementation

        /// <summary>
        /// Serializes a message to a byte array to send to remote application.
        /// This method is synchronized. So, only one thread can call it concurrently.
        /// </summary>
        /// <param name="message">Message to be serialized</param>
        /// <exception cref="CommunicationException">Throws CommunicationException if message is bigger than maximum allowed message length.</exception>
        public byte[] GetBytes(IScsMessage message)
        {
            if (!(message is TrackingDataMessage))
                return null;

            //Serialize the message to a byte array
            var serializedMessage = SerializeMessage(message); 
           
            //Check for message length
            var messageLength = serializedMessage.Length;
            if (messageLength > MaxMessageLength)
            {
                throw new CommunicationException("Message is too big (" + messageLength + " bytes). Max allowed length is " + MaxMessageLength + " bytes.");
            }

            //Create a byte array protocol
            InitSendingProtocol(serializedMessage);
            var bytes = CreateFullPacket();

            //Return serialized message by this protocol
            return bytes;
        }

        /// <summary>
        /// Builds messages from a byte array that is received from remote application.
        /// The Byte array may contain just a part of a message, the protocol must
        /// cumulate bytes to build messages.
        /// This method is synchronized. So, only one thread can call it concurrently.
        /// </summary>
        /// <param name="receivedBytes">Received bytes from remote application</param>
        /// <returns>
        /// List of messages.
        /// Protocol can generate more than one message from a byte array.
        /// Also, if received bytes are not sufficient to build a message, the protocol
        /// may return an empty list (and save bytes to combine with next method call).
        /// </returns>
        public IEnumerable<IScsMessage> CreateMessages(byte[] receivedBytes)
        {
            //Write all received bytes to the _receiveMemoryStream
            _receiveMemoryStream.Write(receivedBytes, 0, receivedBytes.Length);
            //Create a list to collect messages
            var messages = new List<IScsMessage>();
            //Read all available messages and add to messages collection
            while (ReadSingleMessage(messages)) { }
            //Return message list
            return messages;
        }

        /// <summary>
        /// This method is called when connection with remote application is reset (connection is renewing or first connecting).
        /// So, wire protocol must reset itself.
        /// </summary>
        public void Reset()
        {
            if (_receiveMemoryStream.Length > 0)
            {
                _receiveMemoryStream = new MemoryStream();
            }
        }

        #endregion

        #region Proptected virtual methods

        /// <summary>
        /// This method is used to serialize a IScsMessage to a byte array.
        /// This method can be overrided by derived classes to change serialization strategy.
        /// It is a couple with DeserializeMessage method and must be overrided together.
        /// </summary>
        /// <param name="message">Message to be serialized</param>
        /// <returns>
        /// Serialized message bytes.
        /// Does not include length of the message.
        /// </returns>
        protected byte[] SerializeMessage(IScsMessage message)
        {
           if (!(message is TrackingDataMessage))
                return null;
            else
                return ((TrackingDataMessage)message).objTracking.ConvertObjectToByteArray();
        }

        /// <summary>
        /// This method is used to deserialize a IScsMessage from it's bytes.
        /// This method can be overrided by derived classes to change deserialization strategy.
        /// It is a couple with SerializeMessage method and must be overrided together.
        /// </summary>
        /// <param name="bytes">
        /// Bytes of message to be deserialized (does not include message length. It consist
        /// of a single whole message)
        /// </param>
        /// <returns>Deserialized message</returns>
        protected IScsMessage DeserializeMessage(byte[] bytes)
        {
            return new TrackingDataMessage(bytes);
        }

        #endregion

        #region Private methods

        /// <summary>
        /// This method tries to read a single message and add to the messages collection. 
        /// <$$ (2 bytes)><L ( 2 bytes )><ID (7 bytes)><command (2 bytes)><data (160 bytes)><checksum (2 bytes)><\r\n (2 bytes)>
        /// </summary>
        /// <param name="messages">Messages collection to collect messages</param>
        /// <returns>
        /// Returns a boolean value indicates that if there is a need to re-call this method.
        /// </returns>
        /// <exception cref="CommunicationException">Throws CommunicationException if message is bigger than maximum allowed message length.</exception>
        private bool ReadSingleMessage(ICollection<IScsMessage> messages)
        {
            //Go to the begining of the stream
            _receiveMemoryStream.Position = 0;

            //<$$ (2 bytes)><L ( 2 bytes )><ID (7 bytes)><command (2 bytes)><checksum (2 bytes)><\r\n (2 bytes)> = 17 bytes
            //If stream has less than 17 bytes, that means we can not even read length of the message
            //So, return false to wait more bytes from remore application.
            if (_receiveMemoryStream.Length < 4) //read 4 heading bytes: <$$ (2 bytes)><L ( 2 bytes )>
            {
                return false;
            }

            //Read length of the message
            _header = ByteConverter.ReadByteArray(_receiveMemoryStream, 2);
            //if header is different, return immediately
            if (!_header.SequenceEqual(BitConverter.GetBytes((ushort)enumHeader.FromTracking)))
            {
                _receiveMemoryStream = new MemoryStream(); //Clear the stream
                return false;
            }

            _length = ByteConverter.ReadByteArray(_receiveMemoryStream, 2);
            var messageLength = (int)BitConverter.ToInt16(_length,0);
            if (messageLength > MaxMessageLength)
            {
                throw new Exception("Message is too big (" + messageLength + " bytes). Max allowed length is " + MaxMessageLength + " bytes.");
            }

            ////If message is zero-length (It must not be but good approach to check it)
            //if (messageLength == 17)
            //{
            //    //if no more bytes, return immediately
            //    if (_receiveMemoryStream.Length == 4)
            //    {
            //        _receiveMemoryStream = new MemoryStream(); //Clear the stream
            //        return false;
            //    }

            //    //Create a new memory stream from current except first 4-bytes.
            //    var bytes = _receiveMemoryStream.ToArray();
            //    _receiveMemoryStream = new MemoryStream();
            //    _receiveMemoryStream.Write(bytes, 4, bytes.Length - 4);
            //    return true;
            //}

            //If all bytes of the message is not received yet, return to wait more bytes
            if (_receiveMemoryStream.Length < messageLength)
            {
                _receiveMemoryStream.Position = _receiveMemoryStream.Length;
                return false;
            }

            //Read tracking's id
            _id = ByteConverter.ReadByteArray(_receiveMemoryStream, 7);
            //Read command
            _command = ByteConverter.ReadByteArray(_receiveMemoryStream, 2);

            //Read bytes of serialized message and deserialize it
            if (messageLength > 17)
            {
                var serializedMessageBytes = ByteConverter.ReadByteArray(_receiveMemoryStream, messageLength - 17);
                IScsMessage message = DeserializeMessage(serializedMessageBytes);
                try
                {
                    Tracking objTracking = ((TrackingDataMessage)message).objTracking;
                    objTracking.Insert(objTracking);
                }
                catch (Exception ex)
                {
                    throw ex;
                }
                messages.Add(message);
            }

            //Read checksum
            _checksum = ByteConverter.ReadByteArray(_receiveMemoryStream, 2);
            //Read _end character
            _end = ByteConverter.ReadByteArray(_receiveMemoryStream, 2);

            //Read remaining bytes to an array
            var remainingBytes = ByteConverter.ReadByteArray(_receiveMemoryStream, (int)(_receiveMemoryStream.Length - messageLength));

            //Re-create the receive memory stream and write remaining bytes
            _receiveMemoryStream = new MemoryStream();
            _receiveMemoryStream.Write(remainingBytes, 0, remainingBytes.Length);
            
            //Return true to re-call this method to try to read next message
            return (remainingBytes.Length > 4);
        }

       

        #endregion
    }
}
